%%%-------------------------------------------------------------------
%%% File        : smtpcfsm.erl
%%% Author      : Fyodor Ustinov <ufm@ufm.su>
%%% Description : MailEr project
%%%               FSM part of SMTPC module
%%%
%%% Created     :  9 Sep 2009 by Fyodor Ustinov <ufm@ufm.su>
%%%-------------------------------------------------------------------
-module(smtpcfsm).

-behaviour(gen_fsm).

%% API
-export([start_link/1, timeout/2, mx/2, stop/2, ehlo/2, helo/2,
	 do_login/2, plain/2, login/2, 'digest-md5'/2,
	 'digest-md5-s2'/2, 'cram-md5'/2, 
	 mailfrom/2, rcptto/2, data/2]).

%% gen_fsm callbacks
-export([init/1, start/2, state_name/3, handle_event/3,
	 handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).

%-include("smtpc.hrl").

-define(CAST(X), gen_server:cast(S#s.gs, X)).
-define(ASK(X, D),  gen_server:call(S#s.gs, {X, D})).
-define(DTIMEOUTS, 300).
-define(SSLDEFAULT, second).
-define(EHLOMODEDEFAULT, first).

-record(s, {gs = 0,
	    connection_timeout = 0,
	    receive_timeout = 0,
	    send_timeout = 0
	   }).

%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> ok,Pid} | ignore | {error,Error}
%% Description:Creates a gen_fsm process which calls Module:init/1 to
%% initialize. To ensure a synchronized start-up procedure, this function
%% does not return until Module:init/1 has returned.  
%%--------------------------------------------------------------------
start_link(F) ->
    gen_fsm:start_link(?MODULE, [F], []).

%%====================================================================
%% gen_fsm callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, StateName, State} |
%%                         {ok, StateName, State, Timeout} |
%%                         ignore                              |
%%                         {stop, StopReason}                   
%% Description:Whenever a gen_fsm is started using gen_fsm:start/[3,4] or
%% gen_fsm:start_link/3,4, this function is called by the new process to 
%% initialize. 
%%--------------------------------------------------------------------
init([F]) ->
    {ok, start, #s{gs = F}}
.

%%--------------------------------------------------------------------
%% Function: 
%% state_name(Event, State) -> {next_state, NextStateName, NextState}|
%%                             {next_state, NextStateName, 
%%                                NextState, Timeout} |
%%                             {stop, Reason, NewState}
%% Description:There should be one instance of this function for each possible
%% state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_event/2, the instance of this function with the same name as
%% the current state name StateName is called to handle the event. It is also 
%% called if a timeout occurs. 
%%--------------------------------------------------------------------

%% -------------------------------------
%% start of work. Prepare MX list.
%% Next state - ok      -> mx
%%              timeout -> stop
%%              nomx    -> stop
%% -------------------------------------
start(start, S) -> % Start. Ask to prepare MXs
    ?CAST('PrepareMX'),
    {next_state, start, S, ?ASK(dns_timeout, ?DTIMEOUTS) * 1000}
;

start(ok, S) -> % MX ready
io:format("start:ok~n"),
    ?CAST('MX?'),
    {next_state, mx, S}
;

start(timeout, S) -> % DNS timeout
io:format("Start:timeout~n"),
    ?CAST({delay, "DNS timeout."}),
    {next_state, stop, S}
;

start(nomx, S) -> % No acceptable MX
    ?CAST({delay, "Acceptable MX not found."}),
    {next_state, stop, S}
.

%% -------------------------------------
%% mx. Attempt to connect to any mx server
%% Next state - ok      -> ehlo
%%              timeout -> timeout
%%              nomx    -> stop (no more mx)
%%              _       -> mx (ask next mx)
%% -------------------------------------

mx(ok, S) -> % MX ready
io:format("mx:ok~n"),
    Sn = S#s{
	   send_timeout = ?ASK(send_timeout, ?DTIMEOUTS) * 1000,
	   receive_timeout = ?ASK(receive_timeout, ?DTIMEOUTS) * 1000,
	   connection_timeout = ?ASK(connection_timeout, ?DTIMEOUTS) * 1000
	   },

    case ?ASK(ssl_mode, ?SSLDEFAULT) of
	off ->
	    ?CAST(open_nossl);
	first ->
	    ?CAST(open_ssl);
	second ->
	    ?CAST(open_nossl);
	only ->
	    ?CAST(open_ssl)
    end,
    {next_state, mx, Sn}
;

mx({ssl, unable}, S) ->
    case ?ASK(ssl_mode, ?SSLDEFAULT) of
	only ->
	    ?CAST('MX?');
	
	first ->
	    ?CAST(open_nossl);
	
	second ->
	    ?CAST('MX?')
    end,
    {next_state, mx, S}
;

mx({nossl, unable}, S) ->
    case ?ASK(ssl_mode, ?SSLDEFAULT) of
	off ->
	    ?CAST('MX?');
	
	second ->
	    ?CAST(open_ssl);

	first ->
	    ?CAST('MX?')
    end,
    {next_state, mx, S}
;

mx(nomx, S) ->
io:format("mx:nomx~n"),
    ?CAST({delay, "All MXs unavailable."}),
    {next_state, stop, S}
;

mx({ok, "220", _}, S) ->
    Ns = case ?ASK(ehlo_mode, ?EHLOMODEDEFAULT) of
	     only ->
		 ehlo;
	     first ->
		 ehlo;
	     off ->
		 helo
	 end,
    ?CAST(Ns),
    {next_state, Ns, S, S#s.receive_timeout}
;


mx(_D,S) -> % unknown answer
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% ehlo. Attempt to EHLO negotiation
%% Next state - ok              -> additional
%%              timeout         -> timeout
%%              permanent_error -> helo/error (fallback to helo)
%%              _               -> mx (ask next mx)
%% -------------------------------------

ehlo(timeout, S) -> 
    ?CAST(timeout), 
    {next_state, timeout, S}
;


ehlo({ok, "250", _}, S) ->
    ?CAST('parse ehlo'),
    ?CAST('login?'),
    {next_state, do_login, S}
;


ehlo(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.


%% -------------------------------------
%% helo. Attempt to HELO negotiation
%% Next state - ok              -> mailfrom
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------
helo(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

helo({ok, "250", _}, S) ->
     ?CAST(mailfrom),
     {next_state, mailfrom, S, S#s.receive_timeout}
;

helo(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% --------------------------------------
%% do_login. Server can change way to login part
%% or skip this and go directly to 'mailfrom'
%% Next state -> mailfrom
%%               plain
%%               login
%%               'digest-md5'
%%               'cram-md5'
%%
%% --------------------------------------

do_login(NextState, S) ->
    case NextState of
	mailfrom ->
	    ?CAST(mailfrom);

	plain ->
	    ?CAST('auth plain');

	login ->
	    ?CAST('auth login');

	'digest-md5' ->
	    ?CAST('auth digest-md5');

	'cram-md5' ->
	    ?CAST('auth cram-md5');

	_ ->
	    ?CAST(internal_error)
    end,
    {next_state, NextState, S, S#s.receive_timeout}
.

%% -------------------------------------
%% auth plain. Attempt to login use 'plain' method
%% Next state - ok              -> mailfrom
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------


plain(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

plain({ok, _, _}, S) ->
    ?CAST(mailfrom),
    {next_state, mailfrom, S, S#s.receive_timeout}
;
plain(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% auth login. Attempt to login use 'login' method
%% Next state - ok              -> mailfrom
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------

login(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

login({ok, "334", _}, S) ->
    ?CAST('password base64'),
    {next_state, login, S, S#s.receive_timeout}
;

login({ok, _, _}, S) ->
    ?CAST(mailfrom),
    {next_state, mailfrom, S, S#s.receive_timeout}
;
    

login(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% auth digest-md5. Attempt to login use 'digest-md5' method
%% Next state - ok              -> digest-md5-s2 (stage 2)
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------


'digest-md5'(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

'digest-md5'({ok, "334", [_]}, S) ->
    ?CAST('digest-md5 decode and ansver'),
    {next_state, 'digest-md5-s2', S, S#s.receive_timeout}
;

'digest-md5'(_D, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% auth digest-md5. Attempt to login use 'digest-md5' method
%% Second stage
%% Next state - ok              -> mailfrom
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------

'digest-md5-s2'(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;


'digest-md5-s2'({ok, "334", [_]}, S) ->
    ?CAST('digest-md5 check ansver'),
    {next_state, 'digest-md5-s2', S, S#s.receive_timeout}
;

'digest-md5-s2'({ok, [$2, _, _], [_]}, S) ->
    ?CAST(mailfrom),
    {next_state, mailfrom, S, S#s.receive_timeout}
;

'digest-md5-s2'(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.    

%% -------------------------------------
%% auth cram-md5. Attempt to login use 'cram-md5' method
%% Next state - ok              -> mailfrom
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------

'cram-md5'(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

'cram-md5'({ok, "334", _}, S) ->
    ?CAST('cram-md5 decode and ansver'),
    {next_state, 'cram-md5', S, S#s.receive_timeout}
;

'cram-md5'({ok, [$2, _, _], _}, S) ->
    ?CAST(mailfrom),
    {next_state, mailfrom, S, S#s.receive_timeout}
;


'cram-md5'(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% mailfrom
%% Next state - ok              -> rcptto
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------


mailfrom(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

mailfrom({ok, "250", _A}, S) ->
    ?CAST(rcptto),
    {next_state, rcptto, S, S#s.receive_timeout}
;
mailfrom(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.

%% -------------------------------------
%% rcptto
%% Next state - ok              -> data
%%              timeout         -> timeout
%%              permanent_error -> mx (ask next mx)
%%              _               -> mx (ask next mx)
%% -------------------------------------

rcptto(timeout, S) ->
    ?CAST(timeout), 
    {next_state, timeout, S}
;

rcptto({ok, "250", _}, S) ->
    io:format("RCPT TO: Accepted~n"),
    ?CAST(data),
    {next_state, data, S, S#s.receive_timeout}
;

rcptto(_, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
.


data({ok, "354", _}, S) ->
    io:format("DATA: Accepted~n"),
    ?CAST({delay, "OK"}),
    {next_state, stop, S}
.



stop(ok, S) ->    
    io:format("stop: ok~n"),
    {stop, normal, S}
.

timeout(_, S) ->
io:format("--==( FSM: TIMEOUT )==--~n"),
    ?CAST('MX?'),
    {next_state, mx, S}
.

%%--------------------------------------------------------------------
%% Function:
%% state_name(Event, From, State) -> {next_state, NextStateName, NextState} |
%%                                   {next_state, NextStateName, 
%%                                     NextState, Timeout} |
%%                                   {reply, Reply, NextStateName, NextState}|
%%                                   {reply, Reply, NextStateName, 
%%                                    NextState, Timeout} |
%%                                   {stop, Reason, NewState}|
%%                                   {stop, Reason, Reply, NewState}
%% Description: There should be one instance of this function for each
%% possible state name. Whenever a gen_fsm receives an event sent using
%% gen_fsm:sync_send_event/2,3, the instance of this function with the same
%% name as the current state name StateName is called to handle the event.
%%--------------------------------------------------------------------
state_name(_Event, _From, State) ->
    Reply = ok,
    {reply, Reply, state_name, State}.

%%--------------------------------------------------------------------
%% Function: 
%% handle_event(Event, StateName, State) -> {next_state, NextStateName, 
%%						  NextState} |
%%                                          {next_state, NextStateName, 
%%					          NextState, Timeout} |
%%                                          {stop, Reason, NewState}
%% Description: Whenever a gen_fsm receives an event sent using
%% gen_fsm:send_all_state_event/2, this function is called to handle
%% the event.
%%--------------------------------------------------------------------
handle_event(tcp_closed, SName, S) ->
    case SName of
	stop ->
	    {next_state, stop, S};
	_ ->
	    ?CAST('MX?'),
	    {next_state, mx, S}
    end
;

handle_event(temporary_error, _SName, S) ->
    ?CAST('MX?'),
    {next_state, mx, S}
;

handle_event(permanent_error, SName, S) ->
    TNS = case ?ASK(fallback_server, off) of
	      off ->
		  stop;
	      [] ->
		  stop;
	      _ ->
		  mx
	  end,
io:format("FSM: Permanent error. SName == '~p', TNS == '~p'~n",[SName, TNS]),
    case SName of
	ehlo ->
	    case ?ASK(ehlo_mode, ?EHLOMODEDEFAULT) of
		first ->
		    ?CAST(helo),
		    {next_state, helo, S};
		_ ->
		    TNS =:= stop andalso ?CAST(permanent_error),
		    TNS =:= mx   andalso ?CAST('MX?'),
		    {next_state, TNS, S, S#s.receive_timeout}
	    end;
	_ ->
	    TNS =:= stop andalso ?CAST(permanent_error),
	    TNS =:= mx   andalso ?CAST('MX?'),
	    {next_state, TNS, S, S#s.receive_timeout}
    end
;

handle_event(_Event, StateName, State) ->
    {next_state, StateName, State}.

%%--------------------------------------------------------------------
handle_sync_event(_Event, _From, StateName, State) ->
    Reply = ok,
    {reply, Reply, StateName, State}.

%%--------------------------------------------------------------------

handle_info(_Info, StateName, State) ->
    {next_state, StateName, State}.

%%--------------------------------------------------------------------
terminate(_Reason, _StateName, _State) ->
io:format("gen_fsm stopped: ~p~n",[_Reason]),
    ok.

%%--------------------------------------------------------------------
code_change(_OldVsn, StateName, State, _Extra) ->
    {ok, StateName, State}.

%%--------------------------------------------------------------------
